{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "参考资料：\n",
    "- [Python的多线程困境](https://zhuanlan.zhihu.com/p/94909455)\n",
    "- [谈谈Python多线程](https://cloud.tencent.com/developer/article/1489753)\n",
    "- [Python的GIL是什么鬼，多线程性能究竟如何](http://cenalulu.github.io/python/gil-in-python/)\n",
    "- <https://stackoverflow.com/questions/1717393/is-the-operator-thread-safe-in-python>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "   本篇文章假定读者已经有一些操作系统知识的基础，并且几乎不涉及到具体编程，主要研究总结python独特的线程切换调度问题，以及最近用的越来越多的协程的概念和协程切换调度问题。\n",
    "   \n",
    "   需要注意的是，由于Windows系统和Linux系统的一些差别，为统一理解，接下来的所有进程线程讨论我们都是基于Windows系统。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 一、进程、线程概念回顾\n",
    "\n",
    "Windows系统中，进程只是资源的分配单位，而线程才是CPU调度运行的基本单位。也就是说，即使是多个进程的程序，调度依然是按照多个线程去进行调度，由于CPU时间片分配给每个独立调度的线程，拥有四个线程的进程比拥有一个线程的进程会拥有更多的CPU时间片，就像原本一个人干的活，现在四个人同时做，自然也就达到了加速程序的效果。就CPU利用率来讲，如果一个有四个线程的进程运行在一个四核的CPU机器上，那么核的利用率可以达到100%，即所有的核都可以调度运行一个线程， 不会出现一方有难，八方围观的情况。同样，四个单线程进程也能使四核的CPU机器计算资源利用率达到100%，因为每个进程中的线程被独立调度执行。那么如果CPU按照线程独立调用，我们跑python程序的时候，如果同时使用四个线程进行运算是不是会加速程序四倍呢？很不幸的是，并不是这样的。\n",
    "\n",
    "![pic](../assets/funey.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 二、成也GIL，败也GIL\n",
    "\n",
    "\n",
    "**首先需要明确的一点是GIL并不是Python的特性，它是在实现Python解析器(CPython)时所引入的一个概念。**就好比C++是一套语言（语法）标准，但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC，INTEL C++，Visual C++等。Python也一样，同样一段代码可以通过CPython，PyPy，Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python，也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点：**GIL并不是Python的特性，Python完全可以不依赖于GIL。**\n",
    "\n",
    "这里我们主要研究CPython中由于GIL的存在而导致的独特的多线程困境，我们可以先看下GIL的官方说明：\n",
    "\n",
    ">In **CPython**, the global interpreter lock, or GIL, is a mutex that **prevents multiple native threads from executing Python bytecodes at once**. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)\n",
    "\n",
    "简而言之，因为CPython的内存管理不是线程安全的，所以需要加一个全局解释锁来保障Python内部对象是线程安全的。 \n",
    "\n",
    "**为什么会有GIL**\n",
    "\n",
    "由于物理上得限制，各CPU厂商在核心频率上的比赛已经被多核所取代。为了更有效的利用多核处理器的性能，就出现了多线程的编程方式，而随之带来的就是线程间数据一致性和状态同步的困难。即使在CPU内部的Cache也不例外，为了有效解决多份缓存之间的数据同步时各厂商花费了不少心思，也不可避免的带来了一定的性能损失。\n",
    "\n",
    "Python当然也逃不开，为了利用多核，Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁，而当越来越多的代码库开发者接受了这种设定后，他们开始大量依赖这种特性（即默认python内部对象是thread-safe的，无需在实现时考虑额外的内存锁和同步操作）。\n",
    "\n",
    "慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候，发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难？做个类比，像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间，本且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难，那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢？\n",
    "\n",
    "所以简单的说GIL的存在更多的是历史原因。如果推到重来，多线程的问题依然还是要面对，但是至少会比目前GIL这种方式会更优雅。\n",
    "\n",
    "---\n",
    "我们可以看下锁的实现数据结构:\n",
    "\n",
    "![pic](../assets/GIL.jpg)\n",
    "\n",
    "<center>GIL锁数据结构</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "NRMUTEX中的thread_id就表明GIL锁目前被哪个thread拥有，只有一个线程拥有了GIL锁，他才能被解释器解释执行，同一个python进程里面的其他线程就需要等待NRMUTEX的释放。举个例子类比下，正常的多线程程序就像多个人同时干原本一个人干的活，由于多个人同时干，那么自然就会快不少，但是在Python的情况里面，这多个工人都得拿到一张令牌后才能干活，而令牌只有一个，一次只能发给一个工人，其他没拿到令牌的工人就得原地等待，直到拿到令牌为止，这样时时刻刻其实仍然只有最多一个工人在干活。这就会导致如下这个场景的问题出现：\n",
    "\n",
    "![pic](../assets/GIL2.jpg)\n",
    "\n",
    "<center>python多线程困境：线程拿到GIL锁才能运行</center>\n",
    "<br>\n",
    "<br>\n",
    "\n",
    "   比如，一个拥有2个线程的python进程运行在2核的CPU上，我们假设每个线程都只涉及到纯CPU计算，不会被阻塞，只有线程运行的时间片到达才会进行线程切换，每个线程任务完成需要运行4s。我们编号2个线程为T1,T2，编号2个核为C1，C2.如果是两个个非python线程，是可以上做到上图所示的C1调度执行T1，C2调度执行T2， 2个线程**并行**执行，那么上述进程执行结束共需要4s。\n",
    "\n",
    "   但是由于CPython中GIL锁的存在，C1调度执行T1的时候，GIL锁被T1占着，T2拿不到GIL锁，处于阻塞的状态，等到T1执行结束或者执行的字节码行数到了设定的阈值，T1就会释放GIL锁，然后T2获得GIL锁之后再继续执行。这样的结果就是，这个拥有2个纯CPU计算线程的python程序进程运行结束需要8s，因为每个时刻，python进程中永远只有一个线程再被运行。那这就很胃疼了，这么看似乎python的多线程就没用了？\n",
    "   \n",
    "   也不是的，上述情况下多线程没用，是因为我们假定的是每个线程运行代码都是纯CPU计算过程，不会遇到IO等阻塞操作，只在执行结束或者“轮转时间片”到了之后才会被切换，（ 之所以打引号，是因为python的多线程调度的轮转时间片并不是常规CPU时间片，而是按照字节码来算的）。但是如果T1线程有IO操作会被阻塞，会在IO操作前提前释放GIL锁，进而T2线程获得GIL，可以正常被CPU调度执行，这样Python程序进程仍然处于继续运行的状态，而不会像单线程的时候遇到IO会被阻塞等待。话虽如此，除了少部分高端玩家，大部分情况下，我们用python的多线程时，不但没有发挥出多线程的并行威力，反而还承受了多线程的高昂的切换开销以及应对复杂的锁同步的问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 三、Python多线程是否鸡肋\n",
    "\n",
    "**抛开具体应用场景谈“Python多线程是否鸡肋”就是耍流氓了！**\n",
    "\n",
    "## 为什么还需要多线程呢？\n",
    "\n",
    "总结一下，多线程多应用在如下场景:\n",
    "\n",
    "1. 需要运行后台任务但不希望停止主线程的执行\n",
    "    - 定期打印日志\n",
    "    - 图形界面下，主循环需要等待事件\n",
    "2. 分散任务负载\n",
    "    - 高负载任务一般分计算密集型、IO密集型两类。\n",
    "\n",
    "\n",
    "## 计算密集型 vs. IO密集型\n",
    "\n",
    "计算密集型任务的特点是要进行大量的计算，消耗CPU资源，比如计算圆周率、对视频进行高清解码等等，全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成，但是任务越多，花在任务切换的时间就越多，CPU执行任务的效率就越低，所以，要最高效地利用CPU，计算密集型任务同时进行的数量应当等于CPU的核心数。计算密集型任务由于主要消耗CPU资源，因此，代码运行效率至关重要。\n",
    "\n",
    " IO密集型，涉及到网络、磁盘IO的任务都是IO密集型任务，这类任务的特点是CPU消耗很少，任务的大部分时间都在等待IO操作完成（因为IO的速度远远低于CPU和内存的速度）。对于IO密集型任务，任务越多，CPU效率越高，但也有一个限度。**常见的大部分任务都是IO密集型任务**，比如Web应用。IO密集型任务执行期间，99%的时间都花在IO上，花在CPU上的时间很少。\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 计算密集型验证例子\n",
    "\n",
    "Python作为一门脚本语言，本身执行效率极低，完全不适合计算密集型任务的开发。再加上GIL的存在，需要花费大量时间用在线程间的切换，其多线程性能甚至低于单线程。以下是一个验证例子:\n",
    "my_counter()就是一个纯CPU计算代码段，不会被阻塞。当线程运行my_counter()的时候只有在线程结束或者线程轮转时间片到达之后才会释放GIL锁，进行线程切换。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total time: 20.262834310531616\n"
     ]
    }
   ],
   "source": [
    "# 顺序执行的单线程(single_thread.py)\n",
    "\n",
    "from threading import Thread\n",
    "import time\n",
    " \n",
    "def my_counter():\n",
    "    i = 0\n",
    "    for _ in range(100000000):\n",
    "        i = i + 1\n",
    "    return True\n",
    " \n",
    "def main():\n",
    "    thread_array = {}\n",
    "    start_time = time.time()\n",
    "    for tid in range(2):\n",
    "        t = Thread(target=my_counter)\n",
    "        t.start()\n",
    "        t.join()\n",
    "    end_time = time.time()\n",
    "    print(\"Total time: {}\".format(end_time - start_time))\n",
    "\n",
    "    \n",
    "if __name__ == '__main__':\n",
    "    main()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total time: 41.90804672241211\n"
     ]
    }
   ],
   "source": [
    "# 同时执行的两个并发线程(multi_thread.py)\n",
    "\n",
    "from threading import Thread\n",
    "import time\n",
    " \n",
    "def my_counter():\n",
    "    i = 0\n",
    "    for _ in range(100000000):\n",
    "        i = i + 1\n",
    "    return True\n",
    " \n",
    "def main():\n",
    "    thread_array = {}\n",
    "    start_time = time.time()\n",
    "    for tid in range(2):\n",
    "        t = Thread(target=my_counter)\n",
    "        t.start()\n",
    "        thread_array[tid] = t\n",
    "    for i in range(2):\n",
    "        thread_array[i].join()\n",
    "    end_time = time.time()\n",
    "    print(\"Total time: {}\".format(end_time - start_time))\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    main()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "多线程执行更慢了！\n",
    "\n",
    "经过大量测试，Python多线程下一般最多只能占用1.5~2核，完全无法充分利用CPU资源。\n",
    "\n",
    "由于主线程一直在阻塞等待，所以我们不考虑主线程的切换的情况下，顺序执行的过程中，线程切换只发生一次，就是第一个线程运行结束，然后切换到第二个线程进行运行。在第二个程序中，我们同时创建两个子线程，“同时运行”my_counter()，python**程序进程运行过程中，两个子线程会频繁的切换直到结束，操作系统就得不停的保存上下文，切换上下文，带来了很多额外的开销**，两个子线程“同时运行”程序，时间非但没有缩短，反而长了近一倍，**这就是python线程切换带来的开销。**\n",
    "\n",
    "\n",
    "这个例子中，我们看到频繁的线程切换开销还是很高昂的， 这样的话，我们就干脆用python的单线程好了，但是单线程进程运行过程中当线程调用IO被阻塞时任务就停滞了，有没有一种办法，既能让单线程进程即使运行到阻塞操作如读取文件时，线程能不被阻塞，继续完成一些其他的任务，同时还不用承担这么高昂的切换代价呢？有的，那就是**协程**该登场的时候了，后续文章会一步一步讲解。\n",
    "\n",
    "## 小结\n",
    " **在低计算场景(普通后台任务、IO密集型任务)下，Python多线程还是有一点用武之地。**但是计算密集型任务的话，Python多线程是真鸡肋，甚至会严重拖后腿。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 三、锁与线程安全\n",
    "\n",
    "既然有GIL这个语言级的锁，那我们是不是可以不关注锁与线程安全，直接起飞了？\n",
    "\n",
    "且看下面这个例子\n",
    "\n",
    "单操作符不一定是线程安全的"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "ename": "AssertionError",
     "evalue": "971741",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mAssertionError\u001b[0m                            Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-1-d15bc5cbd9eb>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m     15\u001b[0m     \u001b[0mt\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mjoin\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     16\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 17\u001b[1;33m \u001b[1;32massert\u001b[0m \u001b[0mi\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m1000000\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mi\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;31mAssertionError\u001b[0m: 971741"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "\n",
    "i = 0\n",
    "\n",
    "def test():\n",
    "    global i\n",
    "    for x in range(100000):\n",
    "        i += 1\n",
    "\n",
    "threads = [threading.Thread(target=test) for t in range(10)]\n",
    "for t in threads:\n",
    "    t.start()\n",
    "\n",
    "for t in threads:\n",
    "    t.join()\n",
    "\n",
    "assert i == 1000000, i"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "显然失败了。因为高级语言的一条语句执行时都是分为若干条执行码，即使一个简单的计算：i += 1，也是分为4个执行码。\n",
    "\n",
    "- load i\n",
    "- load 1\n",
    "- add\n",
    "- store it back to i\n",
    "\n",
    "**Python解释器默认每100个操作码切换一个活动线程（通过从强制一个线程释放GIL以便另一个线程可以使用）。当100个操作码切换时，就会出现争抢，从而出现线程不安全的情况。此时就需要我们加一个简单的锁。**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import threading\n",
    "i = 0\n",
    "i_lock = threading.Lock()\n",
    "\n",
    "\n",
    "def test():\n",
    "    global i\n",
    "    i_lock.acquire()\n",
    "    try:\n",
    "        for x in range(100000):\n",
    "            i += 1\n",
    "    finally:\n",
    "        i_lock.release()\n",
    "\n",
    "\n",
    "threads = [threading.Thread(target=test) for t in range(10)]\n",
    "for t in threads:\n",
    "    t.start()\n",
    "\n",
    "for t in threads:\n",
    "    t.join()\n",
    "\n",
    "assert i == 1000000, i"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Python的多线程在多核CPU上，只对于IO密集型计算产生正面效果；而当有至少有一个CPU密集型线程存在，那么多线程效率会由于GIL而大幅下降。**\n",
    "\n",
    "# 如何避免受到GIL的影响\n",
    "\n",
    "GIL这么烂，有没有办法绕过呢？我们来看看有哪些现成的方案。\n",
    "\n",
    "**用multiprocess替代Thread**\n",
    "\n",
    "multiprocess库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。**它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL，**因此也不会出现进程之间的GIL争抢。\n",
    "\n",
    "当然multiprocess也不是万能良药。它的引入会**增加程序实现时线程间数据通讯和同步的困难。**就拿计数器来举例子，如果我们要多个线程累加同一个变量，对于thread来说，申明一个global变量，用thread.Lock的context包裹住三行就搞定了。**而multiprocess由于进程之间无法看到对方的数据，只能通过在主线程申明一个Queue，put再get或者用share memory的方法。**这个额外的实现成本使得本来就非常痛苦的多线程程序编码，变得更加痛苦了。具体难点在哪有兴趣的读者可以扩展阅读这篇文章\n",
    "\n",
    "**用其他解析器**\n",
    "\n",
    "之前也提到了既然GIL只是CPython的产物，那么其他解析器是不是更好呢？没错，像JPython和IronPython这样的解析器由于实现语言的特性，他们不需要GIL的帮助。然而由于用了Java/C#用于解析器实现，他们也失去了利用社区众多C语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。毕竟功能和性能大家在初期都会选择前者，Done is better than perfect。\n",
    "\n",
    "**所以没救了么？**\n",
    "\n",
    "当然Python社区也在非常努力的不断改进GIL，甚至是尝试去除GIL。并在各个小版本中有了不少的进步。有兴趣的读者可以扩展阅读[这个Slide](http://www.dabeaz.com/python/UnderstandingGIL.pdf)另一个改进[Reworking the GIL](https://mail.python.org/pipermail/python-dev/2009-October/093321.html)\n",
    "\n",
    "- 将切换颗粒度从基于opcode计数改成基于时间片计数\n",
    "- 避免最近一次释放GIL锁的线程再次被立即调度\n",
    "- 新增线程优先级功能（高优先级线程可以迫使其他线程释放所持有的GIL锁）"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 四、总结\n",
    "Python GIL其实是功能和性能之间权衡后的产物，它尤其存在的合理性，也有较难改变的客观因素。从本分的分析中，我们可以做以下一些简单的总结：\n",
    "\n",
    "- 因为GIL的存在，只有IO Bound场景下得多线程会得到较好的性能\n",
    "- 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块，或者索性用其他语言实现\n",
    "- GIL在较长一段时间内将会继续存在，但是会不断对其进行改进\n",
    "\n",
    "\n",
    "相比Java那种天生面向多线程的语言不同，Python本身多线程就是不太完善的多线程。GIL的存在导致多线程CPU利用效率甚至低于单线程，却仍然要面对锁与线程安全的问题。同时Python语言又不像Java自带多种线程安全的数据类型，增加了多线程编程的复杂度，所以很少有资料大篇幅讲解Python多线程。\n",
    "\n",
    "正如《Python高手之路》所言: (Python)处理好多线程是很难的，其复杂程度意味着与其他方式（**异步事件\\多进程**）相比它是bug的更大来源，而且考虑到通常能够获取的好处很少，**所以最好不要在多线程上浪费太多精力。**"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": false
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
